<?php


namespace app\common\service;

use app\common\enum\MainEnum;
use app\common\exception\ForbiddenException;
use app\common\exception\ParameterFailedException;
use app\common\exception\RuntimeException;

/**
 * Redis单例类
 * @package app\common\service
 *
 * @Author: Li Guo Xin <guoxinlee129@gmail.com>
 * @Date: 2020/4/28
 * @Time: 15:56
 */
class Redis
{
    private static $redis;
    private const AK_EFFECTIVE_SECONDS              = 3600 * 2;     #access_token_effective
    private const AK_FAILING_SECONDS                = 3600 * 24 * 3;#access_token_failing
    private const USER_CREDENTIAL_RESET_PWD_SECONDS = 3600;
    private const TABLE_DATA_SECONDS                = 3600;
    private const LOG_SECONDS                       = 3600 * 24 * 60;

    /**
     * Redis constructor.
     * @throws \Exception
     */
    private function __construct()
    {
        try {
            self::$redis = new \Redis();
            self::$redis->connect(env('redis.host'), env('redis.port'), env('redis.timeout'));
            self::$redis->auth(env('redis.password'));
            //self::$redis->multi(\Redis::PIPELINE);
            //var_dump(self::$redis->info('redis_version'));die;
            //foreach (self::$redis->info() as $item) {
            //    var_dump($item);
            //}
            //die;
            //self::$redis->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_IGBINARY);
            //$a = Db::connect();
            //var_dump($a->table('user1')
            //    ->find());
        } catch (\Exception $e) {
            throw $e;
        }
    }

    /**
     * @return \Redis
     * @throws \Exception
     */
    public static function getRedis(): \Redis
    {
        if (self::$redis === null) {
            new self();
        }
        return self::$redis;
    }

    public static function akKey($app, $ak)
    {
        return $app . '|ak:' . $ak;
    }

    public static function akhKey($app, $user_id)
    {
        return $app . ':' . $user_id . '|akh';
    }

    /**
     * 通过token获取用户ID
     * @param $access_token
     * @param $app
     * @return bool|mixed|string
     * @throws \Exception
     */
    public static function getUserIdByAccessToken($access_token, $app = 'default')
    {
        $userId = self::getRedis()->get(self::akKey($app, $access_token));
        if ($userId) {
            switch ($app) {
                case 'mini':
                    return $userId;
                    break;
                default:
                    //要么有效，要么过期
                    $expire = self::getRedis()->ttl(self::akKey($app, $access_token));
                    if ($expire + self::AK_EFFECTIVE_SECONDS >= self::AK_FAILING_SECONDS) {
                        return $userId;
                    }
                    throw new ForbiddenException(
                        [
                            'msg'  => '令牌已过期',
                            'code' => MainEnum::ERROR_TOKEN_EXPIRE,
                        ]
                    );
            }
        }
        throw new ForbiddenException(
            [
                'msg'  => '令牌有误',
                'code' => MainEnum::ERROR_TOKEN_ERROR,
            ]
        );
    }

    /**
     * 通过token获取用户ID
     * @param $access_token
     * @param $user_id
     * @param string $app 多应用下应该存储多种不同的类型
     * @return bool
     * @throws \Exception
     */
    public static function setAccessToken($access_token, $user_id, $app = 'default'): bool
    {
        //检查是否有旧的token
        $oldAccessToken = self::getRedis()->get(self::akhKey($app, $user_id));
        if ($oldAccessToken) {
            //有旧的，删除
            self::getRedis()->del(self::akKey($app, $oldAccessToken));
            self::getRedis()->del(self::akhKey($app, $user_id));
        }
        switch ($app) {
            case 'mini':
                $options = [
                    'nx',
                ];
                break;
            default:
                $options = [
                    'ex' => self::AK_FAILING_SECONDS,
                    'nx',
                ];
        }
        if (!self::getRedis()->set(self::akKey($app, $access_token), $user_id, $options)) {
            //可能由于生成了重复的token 或 系统问题
            throw new RuntimeException();
        }
        if (!self::getRedis()->set(self::akhKey($app, $user_id), $access_token, $options)) {
            throw new RuntimeException();
        }
        return true;
    }

    /**
     * 重置token的过期时间
     * @param $access_token
     * @param string $app
     * @param $user_id
     * @return bool
     *
     * @Author: Li Guo Xin <guoxinlee129@gmail.com>
     * @Date: 2020-09-23
     * @Time: 19:34
     * @throws \Exception
     */
    public static function addExpireForAccessToken($access_token, $user_id, $app = 'default'): bool
    {
        /**
         * 考虑三个问题：
         * 1.要能知晓是令牌有误 还是 令牌过期
         * 2.重复生成了同一个token的问题
         * 3.用户重复登录 token历史残留的问题
         * 4.验证成功后，增加有效时长
         * 解决方案：
         * 下方解决令牌过期失效知晓问题
         *                    已失效的时间段 Failing                  有效的时间段 Effective
         * |------------------------------------------------------|-----------------------|
         *                             3天                                  2小时
         *
         * 利用redis中的nx不存在的时候才设置，解决重复生成同一个token的问题
         * 利用app|ak_history存储
         *
         * AK_SECONDS_EFFECTIVE 2小时
         * AK_SECONDS_FAILING 3天
         * @key 设计{
         * app|ak:ACCESS_TOKEN
         *              user_id
         * app:id|ak_history
         *              ACCESS_TOKEN
         * }
         * 搜索 app|ak:xxxxx
         * 搜索不到
         * @return 令牌错误
         * 现在的有效期 + AK_EFFECTIVE_SECONDS ?> AK_FAILING_SECONDS 则有效
         *                                    <                    则已失效 @return 已失效
         */
        switch ($app) {
            case 'mini':
                break;
            default:
                self::getRedis()->expire(self::akKey($app, $access_token), self::AK_FAILING_SECONDS);
                self::getRedis()->expire(self::akhKey($app, $user_id), self::AK_FAILING_SECONDS);
        }

        return true;
    }

    /**
     * 设置用户重置密码凭证
     * @param $user_id
     * @throws \Exception
     *
     * @Author: Li Guo Xin <guoxinlee129@gmail.com>
     * @Date: 2020/7/27
     * @Time: 17:19
     */
    public static function setUserResetPasswordCredentials($user_id): void
    {
        self::getRedis()->set('user_credential_reset_pwd:' . $user_id, true, self::USER_CREDENTIAL_RESET_PWD_SECONDS);
    }

    /**
     * 检查用户重置密码凭证
     * @param $user_id
     * @return bool
     * @throws \Exception
     *
     * @Author: Li Guo Xin <guoxinlee129@gmail.com>
     * @Date: 2020/7/27
     * @Time: 17:19
     */
    public static function checkUserResetPasswordCredentials($user_id): bool
    {
        return self::getRedis()->get('user_credential_reset_pwd:' . $user_id) === true;
    }

    /**
     * 获取数据表缓存
     * @param $type
     * @param string $biaoshi
     * @return array|bool|mixed|string
     * @throws \Exception
     *
     * @Author: Li Guo Xin <guoxinlee129@gmail.com>
     * @Date: 2020/7/27
     * @Time: 17:19
     */
    public static function tableGet($type, $biaoshi = '')
    {
        $key = 'table_' . $type;

        switch ($type) {
            case 'users':
                return self::getRedis()->hGetAll($key . ':' . $biaoshi);
                break;
            default:
                return json_decode(self::getRedis()->get($key), true) ?? [];
                break;
        }
    }

    /**
     * 设置数据表缓存
     * @param $type
     * @param $data
     * @throws \Exception
     *
     * @Author: Li Guo Xin <guoxinlee129@gmail.com>
     * @Date: 2020/7/27
     * @Time: 17:59
     */
    public static function tableSet($type, $data): void
    {
        $key = 'table_' . $type;
        switch ($type) {
            case 'users':
                foreach ($data as $userInfo) {
                    self::getRedis()->hMSet($key . ':' . $userInfo['id'], $userInfo);
                    self::getRedis()->expire($key . ':' . $userInfo['id'], self::TABLE_DATA_SECONDS);
                }
                break;
            default:
                self::getRedis()->del($key);
                self::getRedis()->set($key, json_encode($data), self::TABLE_DATA_SECONDS);
                break;
        }
    }

    public static function getStoreConfig($type)
    {
        switch ($type) {
            case 'packagePrice':
                $res = self::getRedis()->get('config_store_' . $type);
                return !$res ? 0 : (float)$res;

            case 'orderPaymentTimedOutMaxTimestamp':
                $res = self::getRedis()->get('config_store_' . $type);
                return !$res ? 3600 * 24 * 7 : (int)$res;
            default:
                return self::getRedis()->get('config_store_' . $type);
        }
    }

    public static function setStoreConfig($type, $v)
    {
        switch ($type) {
            case 'packagePrice':
            case 'orderPaymentTimedOutMaxTimestamp':
            default:
                return self::getRedis()->set('config_store_' . $type, $v);
        }
    }
}
